Skip to main content

Golang

Table of Contents


Basic Concepts

1. What is Go and what are its key features?

Answer: Go (or Golang) is an open-source programming language developed by Google in 2007. Key features include:

  • Compiled language: Fast execution and deployment
  • Statically typed: Type safety with compile-time checking
  • Garbage collected: Automatic memory management
  • Built-in concurrency: Goroutines and channels
  • Simple syntax: Clean, readable code
  • Fast compilation: Quick build times
  • Cross-platform: Compiles to multiple architectures
  • Rich standard library: Comprehensive built-in packages

2. What makes Go different from other programming languages?

Answer:

  • Simplicity: Minimal syntax with only 25 keywords
  • Built-in concurrency: Goroutines are lighter than threads
  • No inheritance: Uses composition over inheritance
  • Interface satisfaction: Implicit interface implementation
  • Fast compilation: Dependency management and linking
  • No generics (until Go 1.18): Encouraged interface usage
  • Opinionated formatting: gofmt enforces consistent style

3. Explain Go's compilation process.

Answer:

  1. Lexical Analysis: Source code → tokens
  2. Parsing: Tokens → Abstract Syntax Tree (AST)
  3. Type Checking: Verify types and interface compliance
  4. Code Generation: AST → machine code
  5. Linking: Combine object files and dependencies
  6. Output: Single binary executable
// Build process
go build main.go // Compile to executable
go run main.go // Compile and run
go install // Build and install to GOPATH/bin

Data Types & Variables

4. What are the basic data types in Go?

Answer:

// Numeric types
var i int = 42
var i8 int8 = 127
var ui uint = 42
var f32 float32 = 3.14
var f64 float64 = 3.14159

// String and character
var s string = "hello"
var r rune = 'A' // Unicode code point
var b byte = 65 // Alias for uint8

// Boolean
var flag bool = true

// Complex numbers
var c complex64 = 1 + 2i
var c128 complex128 = 1 + 2i

5. What's the difference between var, :=, and const?

Answer:

// var - explicit declaration
var name string = "John"
var age int // Zero value: 0
var isActive bool // Zero value: false

// := - short variable declaration (type inference)
name := "John" // Only inside functions
age := 25

// const - compile-time constants
const Pi = 3.14159
const (
StatusOK = 200
StatusNotFound = 404
)

// iota - constant generator
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
)

6. What are zero values in Go?

Answer: Go automatically initializes variables with zero values:

var i int        // 0
var f float64 // 0.0
var s string // ""
var b bool // false
var p *int // nil
var slice []int // nil
var m map[string]int // nil
var ch chan int // nil
var fn func() // nil

Functions & Methods

7. How do you define functions in Go?

Answer:

// Basic function
func add(a, b int) int {
return a + b
}

// Multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}

// Named return values
func calculate(a, b int) (sum, product int) {
sum = a + b
product = a * b
return // naked return
}

// Variadic functions
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}

// Function as type
type Calculator func(int, int) int

func operate(fn Calculator, a, b int) int {
return fn(a, b)
}

8. What's the difference between methods and functions?

Answer:

// Function - standalone
func area(r float64) float64 {
return math.Pi * r * r
}

// Method - associated with a type
type Circle struct {
Radius float64
}

// Method with value receiver
func (c Circle) Area() float64 {
return math.Pi * c.Radius * c.Radius
}

// Method with pointer receiver
func (c *Circle) SetRadius(r float64) {
c.Radius = r
}

// Usage
c := Circle{Radius: 5}
fmt.Println(c.Area()) // Method call
c.SetRadius(10) // Pointer receiver needed for modification

9. When should you use pointer receivers vs value receivers?

Answer: Use pointer receivers when:

  • You need to modify the receiver
  • The receiver is large (copying is expensive)
  • You want to maintain consistency (if one method uses pointer, all should)

Use value receivers when:

  • The receiver is small and immutable
  • The receiver is a basic type, slice, or small struct
  • You want to work with a copy
type Person struct {
Name string
Age int
}

// Value receiver - doesn't modify original
func (p Person) String() string {
return fmt.Sprintf("%s (%d)", p.Name, p.Age)
}

// Pointer receiver - modifies original
func (p *Person) Birthday() {
p.Age++
}

Pointers

10. Explain pointers in Go.

Answer:

// Pointer declaration and usage
var x int = 42
var p *int = &x // p points to x
fmt.Println(*p) // Dereference: prints 42

*p = 100 // Change value through pointer
fmt.Println(x) // prints 100

// Pointer to pointer
var pp **int = &p
fmt.Println(**pp) // prints 100

// nil pointer
var ptr *int
if ptr == nil {
fmt.Println("Pointer is nil")
}

11. What's the difference between passing by value and passing by pointer?

Answer:

// Pass by value - creates copy
func modifyValue(x int) {
x = 100
}

// Pass by pointer - modifies original
func modifyPointer(x *int) {
*x = 100
}

func main() {
num := 42

modifyValue(num)
fmt.Println(num) // Still 42

modifyPointer(&num)
fmt.Println(num) // Now 100
}

Structs & Interfaces

12. How do you define and use structs in Go?

Answer:

// Struct definition
type Person struct {
Name string
Age int
Email string
private string // unexported field
}

// Constructor function
func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
Age: age,
}
}

// Struct literals
p1 := Person{Name: "John", Age: 30}
p2 := Person{"Jane", 25, "jane@example.com", ""}
p3 := &Person{Name: "Bob"} // Other fields get zero values

// Anonymous structs
config := struct {
Host string
Port int
}{
Host: "localhost",
Port: 8080,
}

13. Explain interfaces in Go.

Answer:

// Interface definition
type Writer interface {
Write([]byte) (int, error)
}

type Reader interface {
Read([]byte) (int, error)
}

// Interface composition
type ReadWriter interface {
Reader
Writer
}

// Implementation (implicit)
type FileWriter struct {
filename string
}

func (fw FileWriter) Write(data []byte) (int, error) {
// Implementation here
return len(data), nil
}

// Empty interface
var anything interface{} = "hello"
anything = 42
anything = []int{1, 2, 3}

// Type assertion
if str, ok := anything.(string); ok {
fmt.Println("String:", str)
}

// Type switch
switch v := anything.(type) {
case string:
fmt.Println("String:", v)
case int:
fmt.Println("Integer:", v)
default:
fmt.Printf("Unknown type: %T\n", v)
}

14. What's the difference between empty interface and concrete types?

Answer:

// Empty interface can hold any value
var empty interface{}
empty = "string"
empty = 42
empty = []int{1, 2, 3}

// Concrete type is specific
var str string = "hello"
var num int = 42

// Type assertion needed for empty interface
if s, ok := empty.(string); ok {
fmt.Println(len(s)) // Can use string methods
}

// Concrete types have compile-time type safety
fmt.Println(len(str)) // No type assertion needed

Goroutines & Concurrency

15. What are goroutines and how do they work?

Answer: Goroutines are lightweight threads managed by Go runtime. They're much cheaper than OS threads.

// Starting a goroutine
go func() {
fmt.Println("Running in goroutine")
}()

// Named function as goroutine
func worker(id int) {
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}

func main() {
// Start multiple goroutines
for i := 1; i <= 3; i++ {
go worker(i)
}

// Wait for goroutines to finish
time.Sleep(2 * time.Second)
}

16. How do you coordinate goroutines?

Answer:

import "sync"

// Using WaitGroup
func withWaitGroup() {
var wg sync.WaitGroup

for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Worker %d\n", id)
}(i)
}

wg.Wait()
}

// Using Mutex for shared data
type Counter struct {
mu sync.Mutex
value int
}

func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}

func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}

// Using channels for communication
func withChannels() {
done := make(chan bool)

go func() {
fmt.Println("Working...")
time.Sleep(time.Second)
done <- true
}()

<-done // Wait for completion
}

17. What's the difference between goroutines and threads?

Answer:

AspectGoroutinesOS Threads
Memory2KB initial stack2MB initial stack
CreationVery fastSlower
Context switchingManaged by Go runtimeManaged by OS
CommunicationChannels (CSP model)Shared memory
ScalabilityHundreds of thousandsThousands
SchedulingCooperative (Go scheduler)Preemptive (OS scheduler)

Channels

18. What are channels and how do you use them?

Answer:

// Channel creation
ch := make(chan int) // Unbuffered
buffered := make(chan int, 5) // Buffered

// Sending and receiving
go func() {
ch <- 42 // Send
}()

value := <-ch // Receive

// Directional channels
func sender(ch chan<- int) { // Send-only
ch <- 42
}

func receiver(ch <-chan int) { // Receive-only
value := <-ch
fmt.Println(value)
}

// Closing channels
close(ch)

// Check if channel is closed
value, ok := <-ch
if !ok {
fmt.Println("Channel is closed")
}

// Range over channel
for value := range ch {
fmt.Println(value)
}

19. What's the difference between buffered and unbuffered channels?

Answer:

// Unbuffered channel - synchronous
unbuffered := make(chan int)
go func() {
unbuffered <- 1 // Blocks until received
}()
value := <-unbuffered // Blocks until sent

// Buffered channel - asynchronous
buffered := make(chan int, 3)
buffered <- 1 // Doesn't block (buffer has space)
buffered <- 2
buffered <- 3
// buffered <- 4 // Would block (buffer full)

fmt.Println(<-buffered) // 1
fmt.Println(<-buffered) // 2

20. How do you implement worker pools using channels?

Answer:

func workerPool() {
jobs := make(chan int, 100)
results := make(chan int, 100)

// Start workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}

// Send jobs
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)

// Collect results
for r := 1; r <= 9; r++ {
<-results
}
}

func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
time.Sleep(time.Second)
results <- job * 2
}
}

21. Explain select statement with channels.

Answer:

func selectExample() {
ch1 := make(chan string)
ch2 := make(chan string)

go func() {
time.Sleep(1 * time.Second)
ch1 <- "from ch1"
}()

go func() {
time.Sleep(2 * time.Second)
ch2 <- "from ch2"
}()

for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
case <-time.After(3 * time.Second):
fmt.Println("timeout")
default:
fmt.Println("no communication ready")
time.Sleep(500 * time.Millisecond)
}
}
}

Error Handling

22. How does error handling work in Go?

Answer:

// Error interface
type error interface {
Error() string
}

// Function returning error
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide %v by zero", a)
}
return a / b, nil
}

// Error handling
result, err := divide(10, 0)
if err != nil {
log.Fatal(err)
}

// Multiple error checking
file, err := os.Open("file.txt")
if err != nil {
return err
}
defer file.Close()

data, err := ioutil.ReadAll(file)
if err != nil {
return err
}

23. How do you create custom errors?

Answer:

// Simple custom error
type ValidationError struct {
Field string
Message string
}

func (e ValidationError) Error() string {
return fmt.Sprintf("validation failed for %s: %s", e.Field, e.Message)
}

// Using errors.New
var ErrNotFound = errors.New("item not found")

// Using fmt.Errorf
func processUser(id int) error {
if id < 0 {
return fmt.Errorf("invalid user ID: %d", id)
}
return nil
}

// Error wrapping (Go 1.13+)
func readConfig() error {
_, err := os.Open("config.json")
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}
return nil
}

// Error unwrapping
err := readConfig()
if errors.Is(err, os.ErrNotExist) {
fmt.Println("Config file doesn't exist")
}

24. What are the best practices for error handling in Go?

Answer:

  1. Always check errors: Don't ignore returned errors
  2. Handle errors close to source: Check errors immediately
  3. Provide context: Wrap errors with additional information
  4. Use appropriate error types: Custom errors for different scenarios
  5. Don't panic: Use panic only for unrecoverable errors
// Good error handling
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("failed to open file %q: %w", filename, err)
}
defer func() {
if cerr := file.Close(); cerr != nil {
log.Printf("failed to close file: %v", cerr)
}
}()

// Process file...
return nil
}

Memory Management

25. How does garbage collection work in Go?

Answer: Go uses a tricolor concurrent mark-and-sweep garbage collector:

  1. Mark Phase: Mark all reachable objects
  2. Sweep Phase: Deallocate unmarked objects
  3. Concurrent: Runs alongside application code
// Triggering GC manually (usually not needed)
runtime.GC()

// Memory statistics
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Allocated memory: %d KB\n", m.Alloc/1024)

// Finalizers (use sparingly)
runtime.SetFinalizer(obj, (*Object).cleanup)

26. What causes memory leaks in Go?

Answer: Common causes:

  1. Goroutine leaks: Goroutines that never terminate
  2. Channel leaks: Blocked goroutines waiting on channels
  3. Slice leaks: Holding references to large underlying arrays
  4. Map leaks: Not removing entries from maps
  5. Timer leaks: Not stopping timers
// Goroutine leak example
func leak() {
go func() {
for {
// This goroutine never exits
time.Sleep(time.Second)
}
}()
}

// Fix: provide exit condition
func fixed() {
done := make(chan bool)
go func() {
for {
select {
case <-done:
return
default:
time.Sleep(time.Second)
}
}
}()

// Later: done <- true
}

Package Management

27. How do Go modules work?

Answer:

# Initialize module
go mod init github.com/username/project

# Add dependency
go get github.com/gin-gonic/gin

# Update dependencies
go mod tidy

# Vendor dependencies
go mod vendor

# Download dependencies
go mod download
// go.mod file
module github.com/username/project

go 1.19

require (
github.com/gin-gonic/gin v1.9.1
)

// go.sum contains checksums for security

28. What's the difference between go get and go install?

Answer:

  • go get: Downloads and adds dependencies to current module
  • go install: Builds and installs binaries to $GOPATH/bin
# go get - adds to dependencies
go get github.com/gin-gonic/gin@v1.9.1

# go install - builds binary
go install github.com/username/tool@latest

Testing

29. How do you write tests in Go?

Answer:

// math.go
func Add(a, b int) int {
return a + b
}

// math_test.go
package main

import "testing"

func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5

if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}

// Table-driven tests
func TestAddTable(t *testing.T) {
tests := []struct {
a, b, expected int
}{
{2, 3, 5},
{0, 0, 0},
{-1, 1, 0},
}

for _, test := range tests {
result := Add(test.a, test.b)
if result != test.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
test.a, test.b, result, test.expected)
}
}
}

// Benchmark
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}

30. How do you test concurrent code?

Answer:

func TestConcurrent(t *testing.T) {
// Use race detector: go test -race

counter := 0
var mu sync.Mutex
var wg sync.WaitGroup

for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
counter++
mu.Unlock()
}()
}

wg.Wait()

if counter != 100 {
t.Errorf("Expected 100, got %d", counter)
}
}

Performance & Optimization

31. How do you profile Go applications?

Answer:

import _ "net/http/pprof"

func main() {
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()

// Your application code
}
# CPU profiling
go tool pprof http://localhost:6060/debug/pprof/profile

# Memory profiling
go tool pprof http://localhost:6060/debug/pprof/heap

# Goroutine profiling
go tool pprof http://localhost:6060/debug/pprof/goroutine

32. What are some Go performance best practices?

Answer:

  1. Use pointers for large structs
  2. Prefer slices over arrays for flexibility
  3. Pre-allocate slices when size is known
  4. Use sync.Pool for object reuse
  5. Avoid unnecessary allocations
  6. Use buffered I/O
  7. Profile before optimizing
// Pre-allocate slices
data := make([]int, 0, 1000) // len=0, cap=1000

// Object pooling
var pool = sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}

buf := pool.Get().([]byte)
defer pool.Put(buf)

// Use strings.Builder for string concatenation
var builder strings.Builder
for i := 0; i < 100; i++ {
builder.WriteString("hello ")
}
result := builder.String()

Advanced Topics

33. What is reflection in Go and when should you use it?

Answer:

import "reflect"

func inspectType(x interface{}) {
t := reflect.TypeOf(x)
v := reflect.ValueOf(x)

fmt.Printf("Type: %s\n", t.Name())
fmt.Printf("Kind: %s\n", t.Kind())
fmt.Printf("Value: %v\n", v.Interface())

// For structs
if t.Kind() == reflect.Struct {
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := v.Field(i)
fmt.Printf("Field: %s, Value: %v\n",
field.Name, value.Interface())
}
}
}

// JSON marshaling uses reflection
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}

Use reflection sparingly: It's slower and less type-safe than regular code.

34. Explain context in Go.

Answer:

import "context"

// Context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Context with cancellation
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Context with values
ctx = context.WithValue(ctx, "user-id", 123)

// Using context in functions
func doWork(ctx context.Context) error {
select {
case <-time.After(2 * time.Second):
return nil
case <-ctx.Done():
return ctx.Err() // context.Canceled or context.DeadlineExceeded
}
}

// HTTP with context
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com", nil)

35. What are some common Go interview coding challenges?

Answer:

1. Implement a concurrent safe counter:

type SafeCounter struct {
mu sync.Mutex
v map[string]int
}

func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
c.v[key]++
c.mu.Unlock()
}

func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.v[key]
}

2. Find duplicate elements in slice:

func findDuplicates(nums []int) []int {
seen := make(map[int]bool)
duplicates := make(map[int]bool)
var result []int

for _, num := range nums {
if seen[num] {
if !duplicates[num] {
result = append(result, num)
duplicates[num] = true
}
} else {
seen[num] = true
}
}

return result
}

3. Implement rate limiter:

type RateLimiter struct {
tokens chan struct{}
ticker *time.Ticker
}

func NewRateLimiter(rate int) *RateLimiter {
rl := &RateLimiter{
tokens: make(chan struct{}, rate),
ticker: time.NewTicker(time.Second / time.Duration(rate)),
}

go func() {
for range rl.ticker.C {
select {
case rl.tokens <- struct{}{}:
default:
}
}
}()

return rl
}

func (rl *RateLimiter) Allow() bool {
select {
case <-rl.tokens:
return true
default:
return false
}
}

Quick Reference

Common Gotchas

  1. Loop variable capture: Use function parameter or local variable
  2. Slice append: May create new underlying array
  3. Map iteration: Order is not guaranteed
  4. Nil interface: Interface containing nil pointer is not nil
  5. Defer execution: Deferred functions run in LIFO order

Performance Tips

  • Use make() with capacity for slices and maps
  • Prefer strings.Builder for string concatenation
  • Use sync.Pool for object reuse
  • Profile with go tool pprof
  • Run tests with -race flag

Testing Commands

go test ./...                    # Run all tests
go test -v # Verbose output
go test -race # Race detection
go test -bench=. # Run benchmarks
go test -cover # Coverage report
go test -cpuprofile=cpu.prof # CPU profiling

This document covers the most common Golang interview questions. Practice implementing these concepts and understanding the underlying principles for best results in interviews.